package com.gframework.filemanage;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;


/**
 * 本地文件外部管理操作类.
 * <p>通过本类，你可以将系统本地文件的增删改查交由外部进行管理，例如可以查看目录，修改名称，创建文件/文件夹，读写文件等。
 * <p>当然为了安全起见，你需要设置一个管理的根目录，以保证所有的可操作文件都在这个目录下。
 * 在任何文件操作执行之前，都会对文件的计算后绝对路径进行判断，是否在指定根目录下，以保证安全性。
 * <p>本类将所有Exception异常都在内部转换为了RuntimeException异常
 * 
 * <p>本类对文件的创建进行了兼容性处理，例如目录会自动创建。你无需提前创建好目录进行管理，只需要配置好管理目录，如果目录不存在，那么会自动创建。
 * 
 * <p>本类时线程安全的，同时也是可变的，即你可以随时修改管理的根目录路径。
 * @since 2.1.0
 * @author Ghwolf
 */
public class LocalFileManager {
	
	/**
	 * 管理根目录地址
	 */
	@Nullable
	private String rootDirPath;
	/**
	 * 管理根目录文件对象
	 */
	@Nullable
	private LocalFile rootDir;
	
	
	/**
	 * @param rootDirPath 管理根目录，可以为null
	 * @throws MustBeFolderException 如果路径时一个文件而不是一个目录
	 * @throws GetRealAbsolutePathException 获取文件真实绝对路径失败，通常时因为路径名称错误，例如含有不规范的名称字符
	 */
	public LocalFileManager(@Nullable String rootDirPath){
		if (rootDirPath != null) {
			this.setRootDirPath(rootDirPath);
		}
	}
	
	public LocalFileManager(){
		this(null);
	}
	
	
	/**
	 * 获取根目录文件对象。
	 * @return 返回LocalFile类对象，如果不存在则返回null
	 */
	@Nullable
	public synchronized LocalFile getRootDir() {
		if (rootDir == null) {
			return null ;
		}
		return this.rootDir;
	}
	
	/**
	 * 设置/更新根目录路径.
	 * <p>如果和原始路径一致，则不做任何事情，否则将更新nginx配置文件状态
	 * @param rootDirPath conf目录路径，可以为null，设置null表示不在对任何文件进行管理
	 * @throws MustBeFolderException 如果路径时一个文件而不是一个目录
	 * @throws GetRealAbsolutePathException 获取文件真实绝对路径失败，通常时因为路径名称错误，例如含有不规范的名称字符
	 */
	public synchronized void setRootDirPath(@Nullable String rootDirPath) {
		if (StringUtils.isBlank(rootDirPath)){
			this.rootDirPath = null;
			this.rootDir = null ;
		} else {
			String realPath = getRealPath(new File(rootDirPath));
			if (realPath.equals(this.rootDirPath)) {
				return ;
			}
			LocalFile f = this.getLocalFile(rootDirPath,false);
			if (!f.isFolder()) {
				throw new MustBeFolderException(rootDirPath + " 不是一个目录，不能设置成管理根路径！");
			}
			this.rootDir = f;
			this.rootDirPath = f.getRealPath();
		}
	}
	
	
	/**
	 * 根据一个路径，获取{@link LocalFile}类实例化对象
	 * @param path 文件路径
	 * @return 返回LocalFile类实例化对象，一定不是null
	 * @throws IllegalArgumentException 如果参数null
	 * @throws FileAccessException 此文件不在管理根目录下
	 * @throws GetRealAbsolutePathException 获取文件真实绝对路径失败，通常时因为路径名称错误，例如含有不规范的名称字符
	 */
	public synchronized LocalFile getLocalFile(String path) {
		Assert.notNull(path, "path 不能为null！");
		return getLocalFile(path,true);
	}
	
	private LocalFile getLocalFile(String path,boolean checkAccess) {
		File file = new File(path);
		if (checkAccess) {
			this.checkFileAccess(file);
		}
		String realPath = this.getRealPath(file);
		File realPathFiel = new File(realPath);
		return new LocalFile(realPathFiel, realPath, realPathFiel.getName(), file.isDirectory());
	}
	
	
	/**
	 * 本地文件信息以及操作封装类.
	 * <p>可以直接通过本类方法实现文件/文件夹的增删改查
	 * @since 2.1.0
	 * @author Ghwolf
	 *
	 */
	public class LocalFile {

		/**
		 * 文件真实绝对路径
		 */
		private String realPath;
		/**
		 * 文件名称
		 */
		private String name;
		
		/**
		 * 文件对象
		 */
		private File file ;
		/**
		 * 是否是目录
		 */
		private final boolean folder;

		LocalFile(File file,String realPath, String name, boolean folder) {
			super();
			this.file = file;
			this.realPath = realPath;
			this.name = name;
			this.folder = folder;
		}
		
		/**
		 * 检查路径权限，实时判断当前文件是否在管理根路径下
		 */
		private void checkAccess(){
			if (rootDirPath == null || !realPath.startsWith(rootDirPath)) {
				throw new FileAccessException("无权限操作此文件，因为他不在管理根路径下：" + realPath);
			}
		}
		
		/**
		 * 检查路径权限，实时判断当前文件是否在管理根路径下.
		 * 并且实时判断当前文件是否存在，如果不存在并且是文件夹，则尝试创建，如果创建失败，抛出异常
		 */
		private void checkAccessAndCreateDir(){
			checkAccess();
			if (!file.exists()){
				if (this.folder) {
					if (!this.file.mkdirs()) {
						throw new FileReadWriteException(realPath + " 目录创建失败！");
					}
				} else {
					throw new FileNotFoundRuntimeException(realPath + " 文件不存在！");
				}
			}
		}
		
		/**
		 * 创建一个新的子 文件/文件夹
		 * @param fileName 文件/文件夹名称，不能为null，也不能带有路径分隔符
		 * @param folder 是否目录，true表示新创建的文件是一个目录，否则创建文件
		 * @return 返回创建的新LocalFile类对象
		 * @throws IllegalArgumentException 如果fileName是null或""
		 * @throws MustBeFolderException 当前文件不是一个目录
		 * @throws FileNameIncorrectException 如果文件名称不正确
		 * @throws FileAccessException 文件不在管理根路径下
		 * @throws FileNotFoundRuntimeException 文件不存在
		 * @throws FileReadWriteException 目录创建异常 或 文件创建异常
		 */
		public LocalFile createSubFile(String fileName,boolean folder) {
			synchronized(LocalFileManager.this) {
				checkAccessAndCreateDir();
				if (!this.isFolder()) {
					throw new MustBeFolderException(realPath + " 不是一个目录！不能创建子文件/文件夹");
				}
				if (StringUtils.isEmpty(fileName)) {
					throw new IllegalArgumentException("fileName 不能是null或\"\"，必须是一个有意义的文件名称！");
				}
				checkFileName(fileName);
				
				String path = realPath + File.separatorChar + fileName;
				File file = new File(path);
				
				if (file.exists()) {
					if (folder) {
						if (!file.isDirectory()) {
							throw new FileReadWriteException(file.getAbsolutePath() + " 文件已存在，并且是一个文件，目录创建失败！");
						}
					} else {
						if (file.isDirectory()) {
							throw new FileReadWriteException(file.getAbsolutePath() + " 文件已存在，并且是一个文件夹，文件创建失败！");
						}
					}
				} else {
					// create
					
					if (folder) {
						file.mkdirs();
					} else {
						try {
							file.createNewFile();
						} catch (IOException e) {
							throw new FileReadWriteException(file.getAbsolutePath() + " 文件创建失败！",e);
						}
					}
					
					if (!file.exists()) {
						throw new FileReadWriteException(file.getAbsolutePath() + " 目录/文件创建失败！");
					}
				}

				return new LocalFile(file,path,fileName,folder);
			}
			
		}
		
		/**
		 * 修改当前文件/文件夹的名称
		 * @param newName 新名称
		 * @throws IllegalArgumentException 如果fileName是null或""
		 * @throws FileNameIncorrectException 如果文件名称不正确
		 * @throws FileAccessException 文件不在管理根路径下
		 * @throws FileNotFoundRuntimeException 文件不存在
		 * @throws FileReadWriteException 文件名更新失败 或 目录创建失败
		 */
		public void changeName(String newName) {
			synchronized(LocalFileManager.this) {
				checkAccessAndCreateDir();
				if (StringUtils.isEmpty(newName)) {
					throw new IllegalArgumentException("newName 不能是null或\"\"，必须是一个有意义的文件名称！");
				}
				checkFileName(newName);
				
				String path = file.getParent() + File.separatorChar + newName;
				File newFile = new File(path);
				
				if (newFile.exists()) {
					throw new FileReadWriteException(newFile.getAbsolutePath() + " 文件/文件夹已存在，名称修改失败！");
				}

				try {
					Files.move(this.file.toPath(),newFile.toPath());
				} catch (IOException e) {
					throw new FileReadWriteException(realPath + " 文件/文件夹重命名失败");
				}
				this.realPath = path ;
				this.name = newName ;
			}
		}
		
		/**
		 * 删除当前文件
		 * @throws FileAccessException 文件不在管理根路径下
		 * @throws FileReadWriteException 删除失败，或删除后又立刻出现
		 */
		public void delete() {
			synchronized(LocalFileManager.this) {
				checkAccess();
				if (this.file.isDirectory()) {
					try {
						FileUtils.deleteDirectory(this.file);
					} catch (IOException e) {
						throw new FileReadWriteException(realPath + " 文件/文件夹删除失败！",e);
					}
				} else {
					this.file.delete();
				}
				if (this.file.exists()) {
					throw new FileReadWriteException(realPath + " 文件/文件夹删除失败！");
				}
			}
		}

		/**
		 * 列出当前文件夹下的所有文件.
		 * <p>仅文件夹有效，如果不是文件夹则抛出异常
		 * @return 返回子文件/文件夹对象集合
		 * @throws MustBeFolderException 当前文件不是一个目录
		 * @throws FileAccessException 文件不在管理根路径下
		 * @throws FileNotFoundRuntimeException 文件不存在
		 * @throws FileReadWriteException 目录创建异常，文件读取失败
		 */
		public List<LocalFile> listSub(){
			synchronized(LocalFileManager.this) {
				checkAccessAndCreateDir();
				if (!this.isFolder()) {
					throw new MustBeFolderException(realPath + " 不是一个目录！");
				}
				File[] files = this.file.listFiles();
				if (files == null) {
					throw new FileReadWriteException(realPath + " 目录读取失败！");
				}
				
				List<LocalFile> list = new ArrayList<>(files.length);
				for (File f : files) {
					File realF = new File(LocalFileManager.this.getRealPath(f));
					list.add(new LocalFile(realF,realF.getPath(),realF.getName(),realF.isDirectory()));
				}
				return list ;
			}
		}

		/**
		 * 读取当前文件的内容，并返回byte[]数组.
		 * <p>仅能读取文件内容，如果当前文件是个文件夹，则抛出异常
		 * @return 返回读取到的byte数组，如果文件不存在或没有内容，则数组长度为0
		 * @throws OutOfMemoryError 如果文件过大
		 * @throws FileAccessException 文件不在管理根路径下
		 * @throws FileReadWriteException 不是文件 或 读取异常
		 */
		public byte[] read() {
			synchronized(LocalFileManager.this) {
				checkAccess();
				if (this.folder) {
					throw new FileReadWriteException(realPath + " 当前文件不是一个文件！");
				}
				if (!this.file.exists()) {
					return ArrayUtils.EMPTY_BYTE_ARRAY;
				}
				try {
					return Files.readAllBytes(this.file.toPath());
				} catch(IOException e) {
					throw new FileReadWriteException(realPath + " 文件读取异常！",e);
				}
			}
		}
		
		/**
		 * 读取当前文件的内容，并返回字符串({@code UTF-8}编码).
		 * <p>仅能读取文件内容，如果当前文件是个文件夹，则抛出异常
		 * @return 返回读取到的字符串，如果文件不存在或没有内容，则返回""
		 * @throws OutOfMemoryError 如果文件过大
		 * @throws FileAccessException 文件不在管理根路径下
		 * @throws FileReadWriteException 不是文件 或 读取异常
		 */
		public String readString() {
			return readString(StandardCharsets.UTF_8);
		}
		
		/**
		 * 读取当前文件的内容，并返回字符串.
		 * <p>仅能读取文件内容，如果当前文件是个文件夹，则抛出异常
		 * @param charset 字符串编码，不能为null
		 * @return 返回读取到的字符串，如果文件不存在或没有内容，则返回""
		 * @throws OutOfMemoryError 如果文件过大
		 * @throws FileAccessException 文件不在管理根路径下
		 * @throws FileReadWriteException 不是文件 或 读取异常
		 */
		public String readString(Charset charset) {
			synchronized(LocalFileManager.this) {
				byte[] b = read();
				return b.length == 0 ? "" : new String(b,charset);
			}
		}
		
		
		/**
		 * 写入文件内容，如果文件不存在，则创建文件.
		 * <p>仅能写入文件内容，如果当前文件是个文件夹，则抛出异常
		 * 
		 * @param data 要写入的数据，如果为null，则不做任何事情
		 * @throws FileAccessException 文件不在管理根路径下
		 * @throws FileReadWriteException 文件创建/写入失败
		 */
		public void write(@Nullable byte[] data) {
			if (data == null) return ;
			synchronized(LocalFileManager.this) {
				checkAccess();
				if (this.folder) {
					throw new FileReadWriteException(realPath + " 当前文件不是一个文件！");
				}
				if (!this.file.exists()) {
					this.file.getParentFile().mkdirs();
					try {
						if (!this.file.createNewFile()) {
							throw new FileReadWriteException(realPath + " 文件创建失败！");
						}
					} catch (IOException e) {
						throw new FileReadWriteException(realPath + " 文件创建失败！",e);
					}
				}
				try {
					Files.write(this.file.toPath(), data);
				} catch(IOException e) {
					throw new FileReadWriteException(realPath + " 文件写入异常！",e);
				}
			}
		}
		
		/**
		 * 写入文件内容，如果文件不存在，则创建文件.
		 * <p>仅能写入文件内容，如果当前文件是个文件夹，则抛出异常
		 * 
		 * @param data 要写入的字符串，如果为null，则不做任何事情
		 * @param charset 文件编码，不能为null
		 * @throws FileAccessException 文件不在管理根路径下
		 * @throws FileReadWriteException 文件创建/写入失败
		 */
		public void write(@Nullable String data,Charset charset) {
			if (data == null) return ;
			write(data.getBytes(charset));
		}
		
		/**
		 * 写入文件内容({@code UTF-8}编码)，如果文件不存在，则创建文件.
		 * <p>仅能写入文件内容，如果当前文件是个文件夹，则抛出异常
		 * 
		 * @param data 要写入的字符串，如果为null，则不做任何事情
		 * @throws FileAccessException 文件不在管理根路径下
		 * @throws FileReadWriteException 文件创建/写入失败
		 */
		public void write(@Nullable String data) {
			if (data == null) return ;
			write(data.getBytes(StandardCharsets.UTF_8));
		}
		
		
		// check
		
		/**
		 * 效验文件名是否合法.
		 * <p>不是进行空效验，而是进行合法性效验，例如不能有路径分隔符
		 * <p>如果效验不通过，则抛出异常
		 */
		private void checkFileName(String fileName){
			if (fileName.indexOf('\\') != -1 || fileName.indexOf('/') != -1) {
				throw new FileNameIncorrectException("文件名不应该携带路径分隔符：" + fileName);
			}
			try {
				new File(fileName).toPath();
			} catch (InvalidPathException e) {
				throw new FileNameIncorrectException("文件名称不正确！" + e.getMessage());
			}
		}
		
		// getter

		public String getRealPath() {
			return this.realPath;
		}

		public String getName() {
			return this.name;
		}

		public boolean isFolder() {
			return this.folder;
		}

		@Override
		public String toString() {
			return file.toString();
		}
		
	}
	
	
	/**
	 * 效验文件是否由编辑权限.
	 * <p>这里指的是是否在根目录范围内，而非真实的操作系统权限
	 * <p>如果效验不通过，则抛出异常
	 */
	private void checkFileAccess(File file){
		if (rootDirPath == null || !getRealPath(file).startsWith(rootDirPath)) {
			throw new FileAccessException("无权限操作此文件，因为他不在管理根路径下：" + file.toString());
		}
	}
	
	/**
	 * 获取文件真实绝对路径
	 */
	private String getRealPath(File file) {
		try {
			return file.getCanonicalPath();
		} catch (IOException e) {
			throw new GetRealAbsolutePathException("文件真实路径获取失败：" + file,e);
		}
	}

}
